package org.example;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.ZParams;

import java.util.*;

/**
 * ## Jedis 中 `hset` 与 `zset` 的区别
 *
 * **简介**
 *
 * `hset` 和 `zset` 都是 Jedis 中用于存储数据的命令，但它们之间存在一些关键的区别。
 *
 * **`hset`**
 *
 * * 用于存储哈希表（hash table）中的键值对。
 * * 键可以是任何字符串，值可以是任何字符串或二进制数据。
 * * 哈希表中的键是唯一的，这意味着不能在同一个哈希表中使用相同的键。
 * * 哈希表中的值可以重复，这意味着可以使用相同的键存储多个值。
 *
 * **`zset`**
 *
 * * 用于存储排序集合（sorted set）中的元素。
 * * 元素可以是任何字符串，每个元素都与一个分数（score）相关联。
 * * 排序集合中的元素是唯一的，这意味着不能在同一个排序集合中使用相同的元素。
 * * 排序集合中的元素根据分数进行排序，分数较高的元素排在前面。
 *
 * **比较表**
 *
 * | 特性 | `hset` | `zset` |
 * |---|---|---|
 * | 数据结构 | 哈希表 | 排序集合 |
 * | 键类型 | 字符串 | 字符串 |
 * | 值类型 | 字符串或二进制数据 | 字符串 |
 * | 键唯一性 | 是 | 是 |
 * | 值重复性 | 可以 | 不可以 |
 * | 元素排序 | 否 | 是 |
 *
 * **示例**
 *
 * ```java
 * // 使用 hset 存储键值对
 * Jedis jedis = new Jedis("localhost");
 * jedis.hset("myHash", "key1", "value1");
 * jedis.hset("myHash", "key2", "value2");
 *
 * // 使用 zadd 存储元素和分数
 * jedis.zadd("myZset", 10.0, "member1");
 * jedis.zadd("myZset", 20.0, "member2");
 * ```
 *
 * **注意事项**
 *
 * * 使用 `hset` 时，请注意键的唯一性。不能在同一个哈希表中使用相同的键。
 * * 使用 `zadd` 时，请注意元素的唯一性。不能在同一个排序集合中使用相同的元素。
 * * `hset` 和 `zset` 命令的性能可能会有所不同。哈希表的查找和插入操作通常比排序集合的查找和插入操作更快。
 *
 * **总结**
 *
 * * 使用 `hset` 存储哈希表中的键值对。
 * * 使用 `zset` 存储排序集合中的元素。
 * * 在选择数据结构时，请考虑您的应用程序的具体需求。
 *
 * 希望以上信息对您有所帮助!
 */
public class Chapter01 {
    private static final int ONE_WEEK_IN_SECONDS = 7 * 86400;
    private static final int VOTE_SCORE = 432;
    private static final int ARTICLES_PER_PAGE = 25;

    private static final Jedis conn = new Jedis("localhost", 6379);

    public static void main(String[] args) {
        try{
            new Chapter01().run();
        }catch (Exception e){
            System.out.println(e);
        }finally {
            conn.flushDB();
        }

    }

    public void run() {
        //conn.select(15);

        String articleId = postArticle(
                conn, "username", "A title", "http://www.google.com");
        System.out.println("We posted a new article with id: " + articleId);
        System.out.println("Its HASH looks like:");
        Map<String, String> articleData = conn.hgetAll("article:" + articleId);
        for (Map.Entry<String, String> entry : articleData.entrySet()) {
            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
        }

        System.out.println();

        articleVote(conn, "other_user", "article:" + articleId);
        String votes = conn.hget("article:" + articleId, "votes");
        System.out.println("We voted for the article, it now has votes: " + votes);
        assert Integer.parseInt(votes) > 1;

        System.out.println("The currently highest-scoring articles are:");
        List<Map<String, String>> articles = getArticles(conn, 1);
        printArticles(articles);
        assert articles.size() >= 1;

        addGroups(conn, articleId, new String[]{"new-group"});
        System.out.println("We added the article to a new group, other articles include:");
        articles = getGroupArticles(conn, "new-group", 1);
        printArticles(articles);
        assert articles.size() >= 1;
    }

    /**
     * 总共有四个数据结构
     *
     * 1。存储文章的发布时间   zset
     *      键名:time:
     *          成员: "article：" + articleId
     *          分值: 文章的发布时间
     *
     * 2。存储文章的得分   zset
     *      键名:score:
     *          成员: "article：" + articleId
     *          分值: 文章的得分
     *
     * 3。文章的投票用户   set
     *       键名："voted:" + articleId
     *       键值："user：" + articleId
     *
     * 4。文章的基本信息  set
     *      键名："article:" + articleId
     *      键值：HashMap<String, String>  articleData
     *
     * @param conn
     * @param user
     * @param title
     * @param link
     * @return
     */
    public String postArticle(Jedis conn, String user, String title, String link) {
        //Redis Incr 命令将 key 中储存的数字值增一。
        String articleId = String.valueOf(conn.incr("article:"));

        String voted = "voted:" + articleId;
        //Redis sadd 命令将一个或多个成员元素加入到集合中，已经存在于集合的成员元素将被忽略。 不可重复
        conn.sadd(voted, user);
        conn.expire(voted, ONE_WEEK_IN_SECONDS);

        long now = System.currentTimeMillis() / 1000;
        String article = "article:" + articleId;
        HashMap<String, String> articleData = new HashMap<String, String>();
        articleData.put("title", title);
        articleData.put("link", link);
        articleData.put("user", user);
        articleData.put("now", String.valueOf(now));
        articleData.put("votes", "1");
        /**
         * Redis Hmset 命令用于同时将多个 field-value (字段-值)对设置到哈希表中。
         * 此命令会覆盖哈希表中已存在的字段。
         * 如果哈希表不存在，会创建一个空哈希表，并执行 HMSET 操作。
         */
        conn.hmset(article, articleData);
        conn.zadd("score:", now + VOTE_SCORE, article);
        conn.zadd("time:", now, article);

        return articleId;
    }

    /**
     * 用户对一篇文章进行投票，
     * 如果文章仍然处于可投票的时间范围内，将用户添加到投票用户名单集合里(如果添加成功则表示当前用户是第一次投票)
     * 添加成功则增加文章评分
     * @param conn
     * @param user
     * @param article
     */
    public void articleVote(Jedis conn, String user, String article) {
        long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
        if (conn.zscore("time:", article) < cutoff) {
            return;
        }

        String articleId = article.substring(article.indexOf(':') + 1);
        if (conn.sadd("voted:" + articleId, user) == 1) {
            //zincrby 用于对有序集合成员的分值执行自增操作
            conn.zincrby("score:", VOTE_SCORE, article);
            //文章的基本信息 键值：HashMap<String, String>  articleData
            //hincrBy用于对散列存储的值执行自增操作   votes + 1
            conn.hincrBy(article, "votes", 1);
        }
    }


    public List<Map<String, String>> getArticles(Jedis conn, int page) {
        return getArticles(conn, page, "score:");
    }

    public List<Map<String, String>> getArticles(Jedis conn, int page, String order) {
        int start = (page - 1) * ARTICLES_PER_PAGE;
        int end = start + ARTICLES_PER_PAGE - 1;

        //zrevrange 获取索引区间内的元素   以分值从大到小的顺序取出文章ID  返回Set<键值>
        Set<String> ids = conn.zrevrange(order, start, end);
        List<Map<String, String>> articles = new ArrayList<>();
        for (String id : ids) {
            Map<String, String> articleData = conn.hgetAll(id);
            articleData.put("id", id);
            articles.add(articleData);
        }

        return articles;
    }

    public void addGroups(Jedis conn, String articleId, String[] toAdd) {
        String article = "article:" + articleId;
        for (String group : toAdd) {
            conn.sadd("group:" + group, article);
        }
    }

    public List<Map<String, String>> getGroupArticles(Jedis conn, String group, int page) {
        return getGroupArticles(conn, group, page, "score:");
    }

    public List<Map<String, String>> getGroupArticles(Jedis conn, String group, int page, String order) {
        String key = order + group;
        if (!conn.exists(key)) {
            ZParams params = new ZParams().aggregate(ZParams.Aggregate.MAX);
            conn.zinterstore(key, params, "group:" + group, order);
            conn.expire(key, 60);
        }
        return getArticles(conn, page, key);
    }

    private void printArticles(List<Map<String, String>> articles) {
        for (Map<String, String> article : articles) {
            System.out.println("  id: " + article.get("id"));
            for (Map.Entry<String, String> entry : article.entrySet()) {
                if (entry.getKey().equals("id")) {
                    continue;
                }
                System.out.println("    " + entry.getKey() + ": " + entry.getValue());
            }
        }
    }
}
